Skip to content

Conversation

@gavande1
Copy link

@gavande1 gavande1 commented Jan 7, 2026

🚧 WIP

Motivation for the change, related issues

After upgrading from @php-wasm/node v3.0.22 to v3.0.39, Jest tests that use loadNodeRuntime() directly fail with ESM-related errors.

The per-version packages introduced in #3062 contain JavaScript files that use ESM syntax (import.meta.url), which breaks when loaded in Jest's CommonJS sandbox.

Error:

TypeError: A dynamic import callback was invoked without --experimental-vm-modules

    at getPHPLoaderModule (node_modules/@php-wasm/node/index.cjs:54:7)

Or alternatively:

SyntaxError: Cannot use 'import.meta' outside a module

    /node_modules/@php-wasm/node-8-3/asyncify/php_8_3.js:5
    const require = createRequire(import.meta.url);
                                         ^^^^

This affects downstream projects (like WordPress Studio) that use @php-wasm/node directly in their Jest test suites.

Implementation details

This PR adds a test that verifies @php-wasm/node can be used directly in Jest without:

  • Requiring --experimental-vm-modules
  • Spawning a separate process via runCLI()

The test currently fails - it demonstrates the issue and will pass once a fix is implemented.

Root Cause

The per-version packages (@php-wasm/node-8-3, etc.) contain ESM files:

// @php-wasm/node-8-3/asyncify/php_8_3.js
import { createRequire } from 'module';
const require = createRequire(import.meta.url);  // ESM syntax

When @php-wasm/node/index.cjs dynamically imports these packages, Jest cannot handle the ESM dynamic imports in its CommonJS sandbox.

Related PRs

Testing Instructions (or ideally a Blueprint)

Run the test suite for commonjs-and-jest:

cd packages/playground/test-built-npm-packages/commonjs-and-jest
npm install
npm test

The new test php-wasm-node.spec.ts will fail with the error above, demonstrating the issue.

gavande1 and others added 7 commits January 7, 2026 22:07
Add a test that verifies @php-wasm/node can be used directly in Jest
without requiring --experimental-vm-modules or spawning a child process.

This test currently fails due to ESM syntax (import.meta.url) in the
per-version packages breaking in Jest's CommonJS sandbox.
@bgrgicak bgrgicak force-pushed the fix/jest-esm-compatibility-test branch from 074e47a to f765ae6 Compare January 9, 2026 11:39
@bgrgicak
Copy link
Collaborator

bgrgicak commented Jan 9, 2026

I've been exploring a fix for this issue, which checks if the environment supports dynamic imports and uses require otherwise, by using simple if checks to determine if Jest is running.

As mentioned in the PR description, the Jest VM doesn't allow dynamic imports by default; to make it work, Jest needs to run with a --experimental-vm-modules flag.

Dynamic imports work in both ESM and CommonJS, so from what I can see, this is a Jest-specific issue.

I don't think that we should address it in PHP-wasm by switching between import and require, and see two potential paths forward.

  • We could accept it as is and document that Jest will require the --experimental-vm-modules flat to work with PHP-wasm.
  • We could build a .cjs version of the PHP-wasm JS file, but these files are already 900+ KB, and adding separate cjs files would significantly increase the package size.

Looking at both options, my suggestion is to require the --experimental-vm-modules flag for Jest to work.

Is there a third option that I'm missing? Are there other places besides Jest where dynamic loading is an issue?

@adamziel @brandonpayton @mho22, what do you think?

@mho22
Copy link
Collaborator

mho22 commented Jan 10, 2026

@bgrgicak I fully agree with you.

I think we have another complex option that could make this work. Print separate banners in CJS and ESM during build. This works with ESBuild or Vite. A Vite plugin could be created to add banners for tests.

The last missing piece would be running the different packages locally, probably inside node-es-module-loader/loader.mts and we will maybe have a node-cjs-loader/loaders.cts that would add that part too for CJS mode ?

However, this is a lot more complicated process compared to adding the --experimental-vm-modules flag in Jest.

@bgrgicak
Copy link
Collaborator

bgrgicak commented Jan 14, 2026

If you want to test your local changes with this test, you can use this process:

In one tab run nvm use 23; npm run local-package-repository.

In another cd packages/playground/test-built-npm-packages/commonjs-and-jest
Open package.json and replace the @wp-playground/cli with a URL to that package on the local package repository.
Install the package with npm install.
Run test with npm run test .

@adamziel
Copy link
Collaborator

This error seems to be highly specific to jest:

cloudnik@MacBook-Pro-4 ~/w/A/c/p/p/testcjs (trunk) [1]> npx jest ./                                                                                                                                          (base) 

 RUNS  ./test.spec.js
node:internal/modules/esm/utils:271
    throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG();
          ^

TypeError: A dynamic import callback was invoked without --experimental-vm-modules

(test.spec.js)

it('should load the PHP loader module', () => {
	require('../dist/packages/php-wasm/node-builds/8-3/index.cjs').getPHPLoaderModule().then(console.log, console.error)
})

I couldn't, for the life of me, reproduce that error with vanilla Node 22+ without jest involved. I've tried:

> node                                                                                     (base) 
Welcome to Node.js v22.21.0.
Type ".help" for more information.
> require('./dist/packages/php-wasm/node-builds/8-3/asyncify/php_8_3.js');
[Module: null prototype] {
  dependenciesTotalSize: 27938593,
  dependencyFilename: '/Users/cloudnik/www/Automattic/core/plugins/playground/dist/packages/php-wasm/node-builds/8-3/asyncify/8_3_28/php_8_3.wasm',
  init: [Function: init]
}

It doesn’t break with node test.cjs with package.json where "type": "commonjs" is set and that has the following content

require('../dist/packages/php-wasm/node-builds/8-3/index.cjs').getPHPLoaderModule().then(console.log, console.error)

This also worked (although with some warnings):

esbuild --bundle --target=node22 --platform=node ./test.cjs

npx ts-node also worked.

What are you doing differently, jest?

@adamziel
Copy link
Collaborator

jest seems to be using the node:vm module to run the tested code, and Node.js seems to be much more limited around ESM interop in that context:

CleanShot 2026-01-14 at 14 58 43@2x

@adamziel
Copy link
Collaborator

adamziel commented Jan 14, 2026

Thank you for opening this PR and exploring the space of possibility!

CJS and ESM interop is really difficult to get right, and Jest's custom runtime makes it even more difficult. Given the amount of headache the current CJS+ESM plumbing is already giving us, I'd rather not make it even more complex. Instead, let's just officially make jest unsupported without the --experimental-vm-modules. I've opened #3121 to at least provide clear error messages in typical jest failure modes.

@adamziel adamziel closed this Jan 14, 2026
@gavande1
Copy link
Author

Thanks @bgrgicak and @adamziel for taking look at this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants